内存溢出(Out Of Memory,简称OOM)是指应用系统中存在无法回收的
内存或使用的
内存过多,最终使得程序运行要用到的
内存大于能提供的最大内存。此时
程序就运行不了,系统会提示内存溢出,有时候会
自动关闭软件,重启电脑或者软件后释放掉一部分内存又可以正常运行该软件,而由
系统配置、
数据流、用户代码等原因而导致的内存溢出错误,即使用户重新执行任务依然无法避免。
简介
内存溢出已经是
软件开发历史上存在了近40年的“老大难”问题,像在“
红色代码”病毒事件中表现的那样,它已经成为
黑客攻击企业网络的“
罪魁祸首”。 如在一个域中输入的数据超过了它的要求就会引发数据溢出问题,多余的数据就可以作为
指令在计算机上运行。据有关安全小组称,操作系统中超过50%的
安全漏洞都是由内存溢出引起的,其中大多数与
微软的技术有关。内存溢出错误是
大数据处理平台的常见错误,例如,国际知名的程序开发者问答网站 stackoverflow 上关于“Hadoop out of memory”的问题超过10000个,在
Spark邮件列表上有10%的问题是关于“out of memory”。 内存溢出错误会导致处理数据的任务失败,甚至会引发平台崩溃等严重后果。对于内存溢出大部分的处理方法是重新执行任务,然而, 对于由系统配置、数据流、用户代码等原因而导致的内存溢出错误,即使用户重新执行任务依然无法避免。
内存溢出通俗理解就是
内存不够,是指运行
程序时要求的内存,超出了系统所能分配的范围,从而导致发生内存溢出。一般在运行大型软件时,所需的内存远远超出了主机内安装的内存所承受大小时就会发生这种情况。
当出现内存溢出这种情况,系统一般会提示相关信息,有时候会自动关闭软件甚至会造成设备卡死等现象,重启电脑或者软件后释放掉一部分内存又可以正常运行该软件或游戏一段时间。
常见现象
以Android 开发为例,在开发过程中经常遇到Android内存溢出的意外情况的发生。
以下是国内外总结造成内存溢出的几点现象。
1.大量位图的加载
Bitmap代表一张
位图文件,扩展名是.bmp或者.dip,它是
非压缩格式,其显示效果较好,但缺点就是需要占用大量的存储空间。它是
windows标准格式图形文件,由点组成,每一个点代表一个
像素。每个点可以由多种色彩表示,包括2、4、8、16、24和32位色彩。色彩越高,显示效果越好,但所占用的字节数也就越大。计算一张
Bitmap所占内存大小主要由3个因数有关,即图片宽度,图片长度,单位像素所占用的字节数。大小=图像长度*图片宽度*单位像素占用的字节数。有时候我们需要从网络上获取大量的图片并且展现在view中,但是如果图片较大,一次性加载大量
Bitmap,那么程序可用内存会瞬间增长,引起OOM。
2.位图对象没有及时释放
当程序中需要操作
Bitmap 对象的时候,当它不在被使用的时候,可以调用Bitmap.recycle()方法回收此对象的像素所占用的内存,如果对
Bitmap没有及时释放,在程序长期运行过程中,就很有可能造成OOM意外情况的发生。
3.查询数据库没有关闭游标
程序中经常会进行查询数据库的操作,但是经常会有使用完毕
Cursor后没有关闭的情况。如果我们的查询结果集比较小,对内存的消耗不容易被发现,只有在常时间大量操作的情况下才会复现内存问题,这样就会给以后的测试和问题排查带来困难和风险。
4.构造Adapter时,没有使用缓存的convertView
以构造
ListView的BaseAdapter 为例,在BaseAdapter中提高了方法: publicView getView(int position, View convertView, ViewGroup parent)来向
ListView提供每一个
item所需要的view对象。初始时
ListView 会从BaseAdapter中根据当前的屏幕布局
实例化一定数量的view 对象,同时ListView 会将这些view对象缓存起来。当向上滚动
ListView时,原先位于最上面的list item 的view对象会被回收,然后被用来构造新出现的最下面的listitem.这个构造过程就是由getView()方法完成的,getView()的第二个
形参View convertVicw 就是被缓存起来的listitem的view对象(
初始化时缓存中没有view对象则convertView是null)。如果我们不去使用convertView,而是每次都在getView()中重新实例化一个View对象的话,即浪费资源也浪费时间,也会使得内存占用越来越大。
主要原因
造成这种现象的原因通常有两种:
第一种是由于长期保持某些
资源的引用,垃圾回收器无法回收它,从而使该
资源不能够及时
释放,也称为
内存泄露;
另外一种是当需要保存多个耗用内存过大或当加载单个超大的对象时,该对象的大小超过了当前剩余的可用内存空间。
以Android程序为例:
若所有的引用都是强引用,则大量内存会被占用,最终导致内存溢出。
解决方法:使用弱引用或软引用,软引用的对象在内存不足时可被
GC回收,弱引用的对象在垃圾回收时可被回收。
2.由大量图片显示导致的内存溢出
为解决由大量图片显示造成的内存溢出,可以使用BitmapFactory.Options类,在返回参数时,只返回Bitmap的尺寸大小,而不将其加载到内存中,可有效减少内存溢出。同时在加载完后调用system. gc()通知系统及时回收。
检查在数据库查询中,是否有一次获得全部数据的查询。一般而言,如果一次取十万条记录到内存,就可能引起内存溢出。该问题比较隐蔽,在上线前,数据库中数据较少,通常运行正常,上线后,数据库中数据增多,一次查询即有可能引起内存溢出。因此,对于
数据库查询,尽量采用分页的方式查询。
4.代码中存在死循环或循环产生过多重复对象实体造成的内存溢出
出现这种情况,只能通过查看日志找出产生该问题的原因,检查代码中是否有
死循环、
递归调用,或大循环重复产生的新对象实体。
解决方法
内存溢出虽然很棘手,但也有相应的解决办法,可以按照从易到难,一步步的解决。以Java程序为例:
第一步,就是修改
JVM启动参数,直接增加内存。这一点看上去似乎很简单,但很容易被忽略。JVM默认可以使用的内存为64M,Tomcat默认可以使用的内存为128
MB,对于稍复杂一点的系统就会不够用。在某项目中,就因为启动参数使用的默认值,经常报“Out Of Memory”错误。因此,-Xms,-Xmx参数一定不要忘记加。
第二步,检查
错误日志,查看“Out Of Memory”错误前是否有其它异常或错误。在一个项目中,使用两个数据库连接,其中专用于发送短信的数据库连接使用DBCP
连接池管理,用户为不将短信发出,有意将数据库连接用户名改错,使得日志中有许多数据库连接异常的日志,一段时间后,就出现“Out Of Memory”错误。经分析,这是由于DBCP连接池BUG引起的,数据库连接不上后,没有将连接释放,最终使得DBCP报“Out Of Memory”错误。经过修改正确数据库连接参数后,就没有再出现内存溢出的错误。
查看日志对于分析内存溢出是非常重要的,通过仔细查看日志,分析内存溢出前做过哪些操作,可以大致定位有问题的模块。
第三步,安排有经验的编程人员对代码进行走查和分析,找出可能发生内存溢出的位置。重点排查以下几点:
第四步,使用内存查看工具动态查看内存使用情况。某个项目上线后,每次系统启动两天后,就会出现内存溢出的错误。这种情况一般是
代码中出现了缓慢的内存泄漏,用上面三个步骤解决不了,这就需要使用内存查看工具了。
内存查看工具有许多,比较有名的有:Optimizeit Profiler、JProbeProfiler、JinSight和Java1.5的Jconsole等。它们的基本工作原理大同小异,都是监测
Java程序运行时所有对象的申请、释放等动作,将
内存管理的所有信息进行统计、分析、可视化。开发人员可以根据这些信息判断程序是否有
内存泄漏问题。一般来说,一个正常的系统在其启动完成后其内存的占用量是基本稳定的,而不应该是无限制的增长的。持续地观察系统运行时使用的内存的大小,可以看到在内存使用监控窗口中是基本规则的锯齿形的图线,如果内存的大小持续地增长,则说明系统存在内存泄漏问题。通过间隔一段时间取一次内存快照,然后对内存快照中对象的使用与引用等信息进行比对与分析,可以找出是哪个类的对象在泄漏。
通过以上四个步骤的分析与处理,基本能处理内存溢出的问题。当然,在这些过程中也需要相当的经验与敏感度,需要在实际的开发与调试过程中不断积累。
避免内存溢出
避免内存溢出的常用方法众所周知,以
Android 开发为例,每个
Android应用程序在运行时都有一定的内存限制,限制大小一般为16MB或24MB(视平台而定)。当
应用程序在实际运行过程中没有做到合理、有效利用内存空间,超过该限制大小就会内次溢出。
下面是列举了国内外在
Android应用程序开发过程中应对内存溢出而经常采用的方法。
内存泄露的检测
内存溢出和
内存泄露是两个不同的现象,内存泄露是指长期保持某些资源的引用,垃圾回收器无法回收它,从而造成该资源不能够及时释放,随着程序运行时间的增加,占用存储空间越来越多,致使有效可再利用的存储空间不足,当储存别的资源时引发内存溢出。
内存泄露是造成内存溢出的一个很主要的原因。因此,在实际的开发过程中要坚决杜绝内存泄露的现象发生。由于
Android应用程序是基于虚拟机的,其内存管理都是由Dalivk代为管理,GC回收不是很及时。如果有一个正常的应用程序在其运行稳定后其内存的占用量是不会无限制的增长,是保持在一个稳定的水平。
同样,对任何一个
类的对象的使用个数也有一个相对稳定的上限,没有出现持续增长的情况。当我们持续地观察某个应用程序运行过程中使用
内存的大小和各实例的个数时,如果内存的大小持续增长,则说明系统存在内存泄露情况。比如一个Activity被关掉之后,其内存的引用对象会在下次GC回收的时候通过回收算法计算,如果这部分内存已经属于可回收的对象,那么这些对象会被一并回收,内存未泄露趋势图如图1所示。
在重复开发关闭某个
应用程序的时候,内存一直在向上爬升,也就是说每次关闭这个
Activity 的时候,有些应该释放的内存并没有被释放掉。内存发生泄露的趋势图如图2所示。
采用二级缓冲机制
每次需要加载图片的时候,首先从特定的内存中查找。如果内存中没有再从
SD卡文件中查找,如果没找到,则通过网络获取。当获得来自
网络数据时,先缓冲到底层由硬引用实现的缓冲中(一级缓冲),同时缓冲到文件中(二级
缓冲)。
根据硬引用的特性,当回收垃圾的时候自动执行,人为无法干预,即使抛出OOM错误,致使应用系统异常终止,也不会随意回收具有强引用的对象来解决
内存不足的问题。
假如当前的网络状态很好,下载速度很快的环境中,当快速翻动聊天列表需要快速加载并显示大量图片的时候,由于对这些图片是缓冲在LruCache实现的一级缓冲中的,当内存吃紧的时候一级缓冲自动回收,回收的速度远小于下载并缓冲图片速度,这时候就很容易导致OOM的发生。
等比例缩小位图文件
如果位图文件太大,则可以通过设置
BitmapFactory . Options . inSampleSize(采样率)来实现等比例缩小该文件,并且设置BitmapFactory. Options 的inJustDecodeBounds为true, 先获取到宽高,这时候位图并不会加载到内存中,然后计算缩放比例再加载位图适应view控件,这样可以避免OOM的产生。
优化DalivkVM的堆内存
分配堆(heap)是VM中占用内存最多的部分,通常是通过动态分配来获得。其大小处于动态变化中,当堆实际的利用率偏离设定值时,虚拟机会在GC的时候调整堆的大小,从而使实际占用率呈偏大的趋势靠拢。
强制回收内存的信息
由于Android是采用
Java语言实现,因此
Android的内存回收也和Java内存回收一样的机制:通过GC自动管理内存。该机制是通过不定时检测是否有不被使用的对象,如果有则回收这些对象,释放内存。但是GC的回收时不规律的,人为无法控制的。
通常会通过System . gc( )方法来强制启动GC来回收垃圾,以便减小OOM发生的概率。但该方法只是告诉机器回收垃圾,当也有可能不会立刻回收。具体情况取决于机器当时所处的运行情况。
相关概念
内存泄露
内存泄露是造成内存溢出的其中一个原因,但是
内存泄露不一定会造成内存溢出。简单来说,内存溢出就是占用
内存太大,超过了系统可以承受的范围;而
内存泄露则是由于对程序运行分配的对象回收不及时甚至于脆没有被回收,久而久之,则在系统分配的
堆空间里面产生了很多无用的引用。
这种情况下,系统配置容量再多的
内存空间都有可能发生内存溢出。当
Android中Dalivk启动GarbageCollection(GC)机制进行垃圾回收的时候,
GC会选择一些它了解还存活的对象作为内存遍历的根节点(GC Roots),比方说thread stack中的变量, JNI中的全局变量,zygote中的对象(class loader加载)等,然后开始对heap进行遍历。到最后,部分没有直接或者间接引用到GC Roots的就是需要回收的垃圾,会被GC回收掉。GC只能回收那么没有被引用的对象,如果一直引用,当遍历的时候,系统会默认为该对象仍然处于使用过程中,GC无法回收供其他再次分配使用,但实际上这些被引用的对象对当前应用程序来说,是没有任何意义的,使得实际上可用的内存空间逐渐缩小。
以发生的方式进行分类,
内存泄露可以具体分为如下几类:
(1) 偶发性
内存泄露对造成内存泄露的代码只是在某些特定的环境或者操作过程下才会发生。一般情况下不会发生这种现象。
(2)常发性
内存泄露对造成
内存泄露的代码会被多次执行,每次被执行的时候都会导致一块内存泄露。偶发性和常发性内存泄露是相对而言的。对于使用不同的
测试工具和测试算法,常发性可能会变成偶发性内存泄露,或者偶发性内存泄露也会变成常发性内存泄露。
(3)一次性
内存泄露对造成内存泄露的代码只会被执行一次。比如,在
初始化阶段,在类的
构造函数中分配内存,但是在执行结束的阶段没有释放该内存,从而造成内存泄露的意外情况发生。
(4)隐式
内存泄露程序在运行过程中不停地分配内存,但是没有及时释放,而是再等到执行结束的时候才会释放内存,严格地说,这种情况就会中造成内存泄露的发生。例如对于一个
服务器,需要运行的时间很长,可能会长达几百天,如果存在隐式内存泄露,在最坏情况就会发生内存泄露。
从用户使用应用程序的角度来看,内存泄露是一种常见的现象,它本身不会产生非常重大的危害,甚至有一部分用户根本感觉不到内存泄露的发生。但是真正危害之处在于,这种
内存泄露现象的堆积,最终会消耗尽系统所有的内存。因此,具有良好的编程习惯和采取严格的软件测试对于避免内存泄露是一种非常有效的方式。
缓冲区溢出
当计算机向
缓冲区内填充数据位数时,超过了
缓冲区本身的容量,溢出的数据覆盖在合法数据上。
注意:
缓冲区溢出和内存溢出的区别,前者是溢出后的数据会覆盖到
计算机内存中以前的内容。除非这些被覆盖的内容被保存或能够恢复,否则就会永远丢失。
黑客入侵的一种就是用精心编写的入侵代码(一种恶意程序)使缓冲区溢出,然后用自己预设的方法处理缓冲区,并且执行,从而达到入侵操纵。而后者内存溢出是系统自身内存有限无法满足申请需求。
栈溢出
栈溢出就是
缓冲区溢出的一种。 由于
缓冲区溢出而使得有用的
存储单元被改写,往往会引发不可预料的后果。程序在运行过程中,为了临时存取数据的需要,一般都要分配一些
内存空间,通常称这 些空间为缓冲区。如果向缓冲区中写入超过其本身长度的数据,以致于
缓冲区无法容纳,就会造成缓冲区以外的存储单元被改写,这种现象就称为缓冲区溢出。